<?php

namespace Yolo\Components\Scheduler;

use Exception;
use Yolo\Di\Annotations\Singleton;
use Yolo\Logs\Log;
use Yolo\Utils\Async;
use Yolo\Utils\Console;
use Yolo\Utils\CrontabUtil;

/**
 * Scheduler manager.
 * @author LUSHAOMING
 * @date 2024-12-13
 */
#[Singleton]
class SchedulerManager
{
    /**
     * @var SchedulerInterface[] $schedulers
     */
    private array $schedulers = [];

    public function __construct(
        private readonly Log $log,
    ) {}

    public function addScheduler(SchedulerInterface $scheduler): void
    {
        if (isset($this->schedulers[$scheduler::class])) {
            return;
        }

        $class = get_class($scheduler);

        $this->schedulers[$class] = $scheduler;

        Console::log("Scheduler added: $class");
    }

    public function removeScheduler(SchedulerInterface $scheduler): void
    {
        unset($this->schedulers[$scheduler::class]);
        $this->log->info([
            'action' => 'removeScheduler',
            'message' => 'Scheduler removed',
            'data' => [
                'scheduler' => $scheduler::class,
                'lastExecutionTimestamp' => $scheduler->getLastExecutionTimestamp(),
            ]
        ]);
    }

    public function start(): void
    {
        foreach ($this->schedulers as $scheduler) {
            // 放到后台去跑，防止阻塞
            \Hyperf\Coroutine\go(function () use ($scheduler) {
                $this->runScheduler($scheduler);
            });
        }
    }

    /**
     * Run the scheduler.
     * @param SchedulerInterface $scheduler
     * @return void
     */
    private function runScheduler(SchedulerInterface $scheduler): void
    {
        if ($scheduler->isRunRightAway() && !$scheduler->isRunning()) {
            $this->execute($scheduler);
        }

        while (true) {

            $nextExecutionTimestamp = CrontabUtil::getNextRunTime($scheduler->getConfig());
            $scheduler->setNextExecutionTimestamp($nextExecutionTimestamp);

            // 休眠直到下次执行时间
            Async::sleep($nextExecutionTimestamp);

            if ($scheduler->isRunning()) {
                $this->log->warn([
                    'action' => 'schedulerRunning',
                    'message' => 'Scheduler running, skip this execution',
                    'data' => [
                        'scheduler' => $scheduler::class,
                        'lastExecutionTimestamp' => $scheduler->getLastExecutionTimestamp(),
                    ]
                ]);
                continue;
            }

            $this->execute($scheduler);
        }
    }

    /**
     * Execute the scheduler.
     * @param SchedulerInterface $scheduler
     * @return void
     */
    private function execute(SchedulerInterface $scheduler): void
    {
        try {

            $this->log->info([
                'action' => 'startScheduler',
                'message' => 'Start Scheduler ',
                'data' => [
                    'scheduler' => $scheduler::class,
                    'lastExecutionTimestamp' => $scheduler->getLastExecutionTimestamp(),
                ]
            ]);

            $scheduler->setRunning(true);
            $scheduler->execute();
            $scheduler->setRunning(false);
            $scheduler->setLastExecutionTimestamp(time());

            $this->log->info([
                'action' => 'finishedScheduler',
                'message' => 'Finished Scheduler ',
                'data' => [
                    'scheduler' => $scheduler::class,
                    'lastExecutionTimestamp' => $scheduler->getLastExecutionTimestamp(),
                ]
            ]);

        } catch (Exception $e) {

            $scheduler->setRunning(false);
            $scheduler->setLastExecutionTimestamp(time());

            $this->log->error([
                'action' => 'scheduler',
                'message' => 'Error occurred when scheduler running, message: ' . $e->getMessage(),
                'data' => [
                    'scheduler' => $scheduler::class,
                    'lastExecutionTimestamp' => $scheduler->getLastExecutionTimestamp(),
                    'errors' => $e->getTrace(),
                ]
            ]);
        }
    }
}
